/**
 * Copyright (c) 2008 - TheNittyGritty - Thomas Brekelmans
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package nl.thenittygritty.list
{
	import nl.thenittygritty.iterator.IIterable;
	import nl.thenittygritty.iterator.IIterator;
	import nl.thenittygritty.list.sorter.ISortable;
	import nl.thenittygritty.list.sorter.ISorter;
	import nl.thenittygritty.specification.ISpecification;
	import nl.thenittygritty.utils.ArrayUtils;		

	/**
	 * List offers a more formal way of managing an Array by providing a formal 
	 * access and retrieval API including an iterator to loop through 
	 * the stored data.
	 */
	final public class List implements IIterable, ISortable
	{
		// Constants for retrieving the appropriate integer to match the 
		// desired iterator type (used in #getIterator).
		public static const LIST_ITERATOR:int = 0;
		public static const REVERSED_LIST_ITERATOR:int = 1;
		private var items:Array;

		/**
		 * Creates a new List instance with an optional predefined array of 
		 * elements in it.
		 * @param items	optional Array of items which are added to the list 
		 * during construction.
		 */
		public function List(items:Array = null)
		{
			this.items = items == null ? new Array() : items;
		}

		/**
		 * Adds a given item to the end of the list.
		 */
		public function addItem(item:*):void
		{
			this.items.push(item);
		}

		/**
		 * Removes a given item from the list, throws a ListError if the given 
		 * item is not stored in the list.
		 */
		public function removeItem(item:*):void
		{
			var itemIndex:int = ArrayUtils.getItemIndex(item, this.items);
			
			if (itemIndex >= 0)
			{
				this.items.splice(itemIndex, 1);
			}
			else
			{
				throw new ListError("The supplied item: " + item + 
									" does not exist in this list.");
			}
		}

		/**
		 * Removes the first item from the list and returns it.
		 */
		public function shift():*
		{
			if (this.getLength() > 0)
			{
				return this.items.shift();
			}
			else
			{
				throw new ListError("The list is already empty and cannot be " +
									"shifted anymore.");
			}
		}

		/**
		 * Removes the last item from the list and returns it.
		 */
		public function pop():*
		{
			if (this.getLength() > 0)
			{
				return this.items.pop();
			}
			else
			{
				throw new ListError("The list is already empty and cannot be " +
									"popped anymore.");
			}
		}

		/**
		 * Sets the value of the item at the given index to a new item, throws a 
		 * ListError if the given index is not in a valid range.
		 * @param index The index of the item that'll be changed to the new item.
		 * @param item The new item that will be set at the given index.
		 */
		public function setItemAt(index:int, item:*):void
		{
			if (index >= 0)
			{
				this.items[index] = item;
			}
			else
			{
				throw new ListError("The supplied index: " + index + 
									" is not in a valid range (larger than 0)");
			}
		}

		/**
		 * Returns the item currently stored at the given index, throws a 
		 * ListError if the given index is not in a valid range.
		 */
		public function getItemAt(index:int):*
		{
			if (index >= 0 && index <= (this.getLength() - 1))
			{
				return this.items[index];
			}
			else
			{
				throw new ListError("The supplied index: " + index + 
									" is not in a valid range " +
									"(0 to getLength() - 1).");
			}
		}

		/**
		 * Returns whether or not the given item is currently stored in the list.
		 * @param item The item which is checked to exist in the list.
		 */
		public function contains(item:*):Boolean
		{
			return ArrayUtils.isInArray(item, this.items);
		}

		/**
		 * Combines a given list with this list list and returns the enlonged 
		 * list (not a copy).
		 */
		public function concat(list:List):List
		{
			var iterator:ListIterator = list.getIterator(List.LIST_ITERATOR) 
										as ListIterator;
			for (iterator.rewind();iterator.isValid(); iterator.next())
			{
				this.addItem(iterator.getValue());
			}
			
			return this;
		}

		/**
		 * Compares the list to a given list.
		 * @param list  The list which is compared to this list.
		 */
		public function compare(list:List):Boolean
		{
			if (this.getLength() == list.getLength())
			{
				for (var i:int = 0; i < this.getLength(); ++i)
				{
					if (this.getItemAt(i) != list.getItemAt(i))
					{
						return false;
					}
				}
				
				return true;
			}
			else
			{
				return false;
			}
		}

		/**
		 * Returns all the items which are different between this list and the 
		 * given list into a new list.
		 * @param list The list which is compared to this list.
		 * @param useStrictlyEqualsCheck Optional boolean specifying whether a 
		 * equals (==) or strictly equals (===) is used to compare the data 
		 * in the lists.
		 */
		public function getRemainder(list:List):List
		{
			var remainder:List = new List();
			
			var i:int = 0;
			var j:int = 0;
			
			var listItem:*;
			var isRemaining:Boolean;
			
			for (i = 0; i < this.getLength(); ++i)
			{
				listItem = this.getItemAt(i);
				
				isRemaining = true;
				for (j = 0; j < list.getLength(); ++j)
				{
					if (listItem == list.getItemAt(j))
					{
						isRemaining = false;
						break;
					}
				}
				
				if (isRemaining) remainder.addItem(listItem);
			}
			
			for (i = 0; i < list.getLength(); ++i)
			{
				listItem = list.getItemAt(i);
				
				isRemaining = true;
				for (j = 0; j < this.getLength(); ++j)
				{
					if (listItem == this.getItemAt(j))
					{
						isRemaining = false;
						break;
					}
				}
				
				if (isRemaining) remainder.addItem(listItem);
			}
			
			return remainder;
		}

		/**
		 * Removes all the stored items from the list.
		 */
		public function clear():void
		{
			this.items = new Array();
		}

		/**
		 * Returns the length of the list.
		 */
		public function getLength():int
		{
			return this.items.length;
		}

		/**
		 * Returns a new iterator for the list specified by the given type id.
		 * @param iteratorTypeId An integer indicating which kind of iterator 
		 * you would like to get (use constants on the List class to match the 
		 * right integer with the right type of iterator).
		 */
		public function getIterator(iteratorTypeId:int = 0):IIterator
		{
			switch (iteratorTypeId)
			{
				case List.LIST_ITERATOR:
					return new ListIterator(this.items);
				case List.REVERSED_LIST_ITERATOR:
					return new ReversedListIterator(this.items);
				default:
					return new ListIterator(this.items);
			}
		}

		/**
		 * Selects all items in the list which are satisfied by the given 
		 * specification ands returns those in a new List.
		 * @param specification A specification to which each item in the list 
		 * is checked against.
		 */
		public function selectBy(specification:ISpecification):List
		{
			var selection:List = new List();
			
			var iterator:IIterator = this.getIterator(List.LIST_ITERATOR);
			while(iterator.isValid())
			{
				if (specification.isSatisfiedBy(iterator.getValue()))
				{
					selection.addItem(iterator.getValue());
				}
				
				iterator.next();
			}
			
			return selection;
		}
		
		/**
		 * Sorts this list with a given ISorter implementation and returns it.
		 */
		public function sortBy(sorter:ISorter):List
		{
			return sorter.sort(this);	
		}

		/**
		 * Returns a human readable (multilined) String with information about 
		 * the list and all the stored items.
		 */
		public function dump():String
		{
			var output:String = this.toString() + " containing " + 
								this.getLength() + " item(s): \n";
			
			var iterator:IIterator = this.getIterator(List.LIST_ITERATOR);
			while(iterator.isValid())
			{
				output += "\t" + iterator.getValue() + "\n";
				
				iterator.next();
			}
			
			return output;
		}
		
		/**
		 * Returns the name of the class in a String.
		 * Use the #dump method to debug the contents of the list.
		 */
		public function toString():String
		{
			return "List";
		}
	}
}